AWS LambdaでFFmpeg/FFprobeを使ってみた
こんにちは、豊島です。
はじめに
Lambdaで動画/音声ファイルを処理したいシーンで有効なffmpeg-lambda-layer
を紹介します。
今回は動画のフォーマット
と再生時間
を取得することを目的としますが、
FFmpegには他にも
- 動画ファイルの合成、リサイズ
- ウォーターマークの挿入
- 動画から音声ファイルを生成
- 動画/音声のフォーマットの変換(MP4->AVIなど)
- サムネイルの取得
など、多くの機能が用意されています。
気になる方はFFmpegの公式ドキュメントをご確認ください。
なお、今回はFFprobeコマンドの実行にfluent-ffmpegというパッケージを利用します。
Lambda Layerをデプロイ
AWS verified authorとして用意されているApplicationをデプロイします。
-
Deployをクリック
-
テンプレートやアクセス許可、ライセンスが表示されるので確認してデプロイを実行
-
デプロイ完了
Lambda Layerを設定する
今回はAmplify Gen2のFunctionを使って対象のLambdaにLayerを設定します。
import { defineFunction } from "@aws-amplify/backend"
export const ffmpegSample = defineFunction({
name: "ffmpegSample",
entry: "./handler.ts",
layers: {
ffmpeg: "arn:aws:lambda:{region}:{accountId}:layer:ffmpeg:1"
}
})
S3バケットを作成、Lambdaに権限を付与する
こちらも同じくAmplify Gen2のStorage(S3)を使っていきます。
動画ファイルを取得するためs3:GetObject
が付与されるようにしてください。
import { defineStorage } from "@aws-amplify/backend"
import { ffmpegSample } from "../functions/ffmpegSample/resource"
export const storage = defineStorage({
name: "amplifyDevDrive",
access: (allow) => ({
"ffmpegTest/*": [allow.resource(ffmpegSample).to(["get"])]
})
})
FFprobeコマンドを実行する
- 対象のバケットから動画ファイルをエフェメラルストレージへダウンロード
ffprobe
コマンドを実行- エフェメラルストレージのファイルを削除
の流れで処理をしていきます
サンプルコード
import fs from "fs"
import path from "path"
import { promisify } from "util"
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"
import type { Handler } from "aws-lambda"
import ffmpeg from "fluent-ffmpeg"
import { Readable } from "stream"
const s3Client = new S3Client({ region: process.env.AWS_REGION })
const unlinkAsync = promisify(fs.unlink)
export const handler: Handler = async () => {
const bucket = {バケット名}
const key = {オブジェクトキー}
const tempFilePath = path.join("/tmp", "video.mp4")
try {
const { Body } = await s3Client.send(
new GetObjectCommand({ Bucket: bucket, Key: key })
)
if (!Body) {
throw new Error()
}
const readableStream = Body as Readable
await new Promise((resolve, reject) => {
const writeStream = fs.createWriteStream(tempFilePath)
readableStream.pipe(writeStream)
writeStream.on("finish", resolve)
writeStream.on("error", reject)
})
const metadata = await new Promise((resolve, reject) => {
ffmpeg.ffprobe(tempFilePath, (err, metadata) => {
if (err) {
console.error("ffprobe error:", err)
reject(err)
} else {
console.log("ffprobe result:", JSON.stringify(metadata, null, 2))
resolve(metadata)
}
})
})
await unlinkAsync(tempFilePath)
console.log(`Temporary file ${tempFilePath} deleted`)
return {
statusCode: 200,
body: JSON.stringify(metadata)
}
} catch (error) {
console.error("Error:", error)
try {
await unlinkAsync(tempFilePath)
console.log(`Temporary file ${tempFilePath} deleted after error`)
} catch (unlinkError) {
console.error("Error deleting temporary file:", unlinkError)
}
return {
statusCode: 500,
body: JSON.stringify({
error: "An error occurred while processing the video"
})
}
}
}
実行結果を確認する
format
の項目にformat_name
(フォーマット), duration
(再生時間)が存在することを確認できました。
ちなみにformat_name
, format_long_name
は動画ファイルを解析した結果が出力されるため、拡張子を変更しただけの動画ファイルであっても本来の形式を取得することができます。
{
"streams": [
{
"index": 0,
"codec_name": "h264",
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"profile": "Baseline",
"codec_type": "video",
"codec_time_base": "23151/1388000",
"codec_tag_string": "avc1",
"codec_tag": "0x31637661",
"width": 1280,
"height": 720,
"coded_width": 1280,
"coded_height": 720,
"has_b_frames": 0,
"sample_aspect_ratio": "1:1",
"display_aspect_ratio": "16:9",
"pix_fmt": "yuv420p",
"level": 31,
"color_range": "tv",
"color_space": "smpte170m",
"color_transfer": "bt709",
"color_primaries": "bt709",
"chroma_location": "topleft",
"field_order": "unknown",
"timecode": "N/A",
"refs": 1,
"is_avc": "true",
"nal_length_size": 4,
"id": "N/A",
"r_frame_rate": "359/12",
"avg_frame_rate": "694000/23151",
"time_base": "1/16000",
"start_pts": 224,
"start_time": 0.014,
"duration_ts": 185208,
"duration": 11.5755,
"bit_rate": 460477,
"max_bit_rate": "N/A",
"bits_per_raw_sample": 8,
"nb_frames": 347,
"nb_read_frames": "N/A",
"nb_read_packets": "N/A",
"tags": {
"language": "eng",
"handler_name": "VideoHandler"
},
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
}
},
{
"index": 1,
"codec_name": "aac",
"codec_long_name": "AAC (Advanced Audio Coding)",
"profile": "LC",
"codec_type": "audio",
"codec_time_base": "1/48000",
"codec_tag_string": "mp4a",
"codec_tag": "0x6134706d",
"sample_fmt": "fltp",
"sample_rate": 48000,
"channels": 1,
"channel_layout": "mono",
"bits_per_sample": 0,
"id": "N/A",
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/48000",
"start_pts": 0,
"start_time": 0,
"duration_ts": 555840,
"duration": 11.58,
"bit_rate": 69630,
"max_bit_rate": 69502,
"bits_per_raw_sample": "N/A",
"nb_frames": 544,
"nb_read_frames": "N/A",
"nb_read_packets": "N/A",
"tags": {
"language": "eng",
"handler_name": "SoundHandler"
},
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
}
}
],
"format": {
"filename": "/tmp/video.mp4",
"nb_streams": 2,
"nb_programs": 0,
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"format_long_name": "QuickTime / MOV",
"start_time": 0,
"duration": 11.59,
"size": 780881,
"bit_rate": 539003,
"probe_score": 100,
"tags": {
"major_brand": "isom",
"minor_version": "512",
"compatible_brands": "isomiso2avc1mp41",
"encoder": "Lavf58.76.100"
}
},
"chapters": []
}
最後に
LambdaでFFmpeg/FFprobeを使ってみました。
動画/音声ファイルをLambdaで処理したい方の参考になると幸いです。